jQuery.fn.animateWithEvent = ( function( $ ) {
	'use strict';

	/**
	 * Same as `jQuery.fn.animate` or any other animation function with the difference that for each
	 * each element to be animated, a `jQuery.AnimationEvent` will be created. The `AnimationEvent`
	 * instance will be available as first parameter in the `startCallback` which will be called for
	 * each element's animation when the animation is about to start.
	 *
	 * The `startCallback` can be used to trigger an event, stating that an animation is about to be
	 * to be executed. In the event the `AnimationEvent` can be used to allow event listeners to add
	 * specialized callbacks per animation stage.
	 *
	 *     @example
	 *     $.animationWithEvent(
	 *         'mywidgetsgreatanimation',
	 *         'fadeIn',
	 *         { duration: 200 },
	 *         function( animationEvent ) {
	 *             self._trigger( 'animation', animationEvent );
	 *         }
	 *     );
	 *
	 * @member jQuery.fn
	 * @method animateWithEvent
	 * @uses jQuery.AnimationEvent
	 * @license GNU GPL v2+
	 * @author Daniel Werner < daniel.a.r.werner@gmail.com >
	 *
	 * @constructor
	 *
	 * @param {string} animationPurpose Will be forwarded to `jQuery.AnimationEvent`.
	 * @param {string|Object} animationProperties Name of a `jQuery.fn` member which is dedicated
	 *        to some animation (e.g. `fadeIn`) and takes an `options` argument. Can also be an
	 *        object of properties to animate, in this case `jQuery.fn.animate` will be used.
	 * @param {Object} [options={}] Options passed to the animation (`duration`, `easing` etc.).
	 * @param {Function} [startCallback=jQuery.noop] Callback which will be fired before the
	 *        animation starts.
	 *        This is different from the `options.start` callback since it will be passed a
	 *        `jQuery.AnimationEvent` instance as parameter. Also, the callback will be triggered
	 *        before any `options.start` callback.
	 * @param {jQuery.AnimationEvent} startCallback.event
	 * @return {jQuery}
	 *
	 * @throws {Error} if `animationProperties` is a string but not a member of `jQuery.fn`.
	 */
	return function AnimateWithEvent(
		animationPurpose,
		animationProperties,
		options,
		startCallback
	) {
		var animationFunction;
		if ( typeof animationProperties !== 'string' ) {
			// Custom animation, forward to jQuery.fn.animate( animationProperties, options )
			animationFunction = 'animate';
			animationProperties = animationProperties || {}; // allow "empty" animation
		} else {
			// Predefined animation, e.g. "fadeIn" would forward to jQuery.fn.fadeIn( options )
			animationFunction = animationProperties;
			animationProperties = false;
			if ( !$.isFunction( $.fn[ animationFunction ] ) ) {
				throw new Error( 'jQuery.fn."' + animationFunction + '" is not a function.' );
			}
		}

		if ( $.isFunction( options ) || !options ) {
			startCallback = options;
			options = {};
		}
		startCallback = startCallback || $.noop;

		$.each( this, function( i, elem ) {
			var animationEvent = $.AnimationEvent( animationPurpose );

			// The animation options generated by the event will have all animation stage fields
			// defined with callbacks that will fire the related callbacks registered to the
			// animationEvent's "animationCallbacks" field in the future.
			var animationOptions = animationEvent.animationOptions( $.extend( {}, options, {
				start: function() {
					// startCallback could for example trigger an "animation" event within a widget.
					// All event listeners can then register their callbacks for different animation
					// stages to animationEvent.animationCallbacks.
					startCallback.call( this, animationEvent );
					if ( options.start ) {
						options.start.apply( this, arguments );
					}
				}
			} ) );

			if ( animationProperties ) {
				// animate()
				$( elem )[ animationFunction ]( animationProperties, animationOptions );
			} else {
				// Any dedicated animation function, e.g. "fadeIn".
				$( elem )[ animationFunction ]( animationOptions );
			}
		} );
		return this;
	};

}( jQuery ) );
